16.2 初始化
因为内存分配器和垃圾回收算法都依赖连续地址,所以在初始化阶段,预先保留了很大的一段虚拟地址空间。
注意:保留地址空间,并不会分配内存。
该段空间被划分成三个区域:
页所属span指针数组 GC标记位图 用户内存分配区域 +-------------------+--------------------+----------------------------------------------+ |spans 512MB |bitmap 32GB |arena 512GB | +-------------------+--------------------+----------------------------------------------+ spans_mapped bitmap_mapped arena_start arena_used arena_end
可分配区域从Go 1.4的128 GB提高到512 GB。
简单点说,就是用三个数组组成一个高性能内存管理结构。
1.使用arena地址向操作系统申请内存,其大小决定了可分配用户内存的上限。
2.位图bitmap为每个对象提供4 bit标记位,用以保存指针、GC标记等信息。
3.创建span时,按页填充对应spans空间。在回收object时,只须将其地址按页对齐后就可找到所属span。分配器还用此访问相邻span,做合并操作。
任何arena区域的地址,只要将其偏移量配以不同步幅和起始位置,就可快速访问与之对应的spans、bitmap数据。最关键的是,这三个数组可以按需同步线性扩张,无须预先分配内存。
这些区域相关属性被保存在heap里,其中包括递进的分配位置mapped/used。
mheap.go
type mheap struct{ spans **mspan spans_mapped uintptr
bitmap uintptr bitmap_mapped uintptr
arena_start uintptr arena_used uintptr arena_end uintptr arena_reserved bool }
初始化工作很简单:
1.创建对象规格大小对照表。
2.计算相关区域大小,并尝试从某个指定位置开始保留地址空间。
3.在heap里保存区域信息,包括起始位置和大小。
4.初始化heap其他属性。
malloc.go
func mallocinit() { // 初始化规格对照表 initSizes()
…
//64位系统 if ptrSize8&& (limit0||limit>1<<30) { // 计算相关区域大小 arenaSize:=round(_MaxMem, _PageSize) bitmapSize=arenaSize/ (ptrSize8/4) spansSize=arenaSize/ _PageSizeptrSize spansSize=round(spansSize, _PageSize)
// 尝试从0xc000000000开始设置保留地址
// 如果失败,则尝试0x1c000000000~0x7fc000000000
for i:=0;i<=0x7f;i++ {
switch{
case GOARCH== "arm64" &&GOOS== "darwin":
p=uintptr(i)<<40|uintptrMask&(0x0013<<28)
case GOARCH== "arm64":
p=uintptr(i)<<40|uintptrMask&(0x0040<<32)
default:
p=uintptr(i)<<40|uintptrMask&(0x00c0<<32)
}
// 计算整个区域大小,并从指定位置开始保留地址空间
pSize=bitmapSize+spansSize+arenaSize+ _PageSize
p=uintptr(sysReserve(unsafe.Pointer(p),pSize, &reserved))
if p!=0{
break
}
}
}
// 按页对齐 p1:=round(p, _PageSize)
// 保存相关属性 mheap_.spans= (**mspan)(unsafe.Pointer(p1)) mheap_.bitmap=p1+spansSize mheap_.arena_start=p1+ (spansSize+bitmapSize) mheap_.arena_used=mheap_.arena_start mheap_.arena_end=p+pSize mheap_.arena_reserved=reserved // 非指定起始地址,备用地址标记
…
// 初始化heap mHeap_Init(&mheap_,spansSize)
// 为当前线程绑定cache对象 g :=getg() g.m.mcache=allocmcache() }
区域所指定的起始位置,在不同平台会有一些差异。这个无关紧要,实际上我们关心的是保留地址操作细节。
mem_linux.go
func sysReserve(v unsafe.Pointer,n uintptr,reserved*bool)unsafe.Pointer{ if ptrSize==8&&uint64(n) >1<<32{ p:=mmap_fixed(v,64<<10, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1,0) if p!=v{ if uintptr(p) >=4096{ munmap(p,64<<10) } return nil } munmap(p,64<<10) *reserved=false return v } }
func mmap_fixed(v unsafe.Pointer,n uintptr,prot,flags,fd int32,offset uint32) … { p:=mmap(v,n,prot,flags,fd,offset) if p!=v&&addrspace_free(v,n) { if uintptr(p) >4096{ munmap(p,n) } p=mmap(v,n,prot,flags|_MAP_FIXED,fd,offset) } return p }
对系统编程稍有了解的都知道mmap的用途。
函数mmap要求操作系统内核创建新的虚拟存储器区域,可指定起始地址和长度。Windows没有此函数,对应API是VirtualAlloc。
PORT_NONE:页面无法访问。
MAP_FIXED:必须使用指定起始地址。
另外,作为内存管理的全局根对象heap,其相关属性也必须初始化。
mheap.go
func mHeap_Init(h*mheap,spans_size uintptr) { // 初始化几个用于管理用途的固定分配器(参见本章后续内容)
// 初始化相关属性 for i:=range h.free{ mSpanList_Init(&h.free[i]) mSpanList_Init(&h.busy[i]) }
mSpanList_Init(&h.freelarge) mSpanList_Init(&h.busylarge)
// 创建central for i:=range h.central{ mCentral_Init(&h.central[i].mcentral,int32(i)) }
// 将全局变量h_spans指向heap.spans sp:= (*slice)(unsafe.Pointer(&h_spans)) sp.array=unsafe.Pointer(h.spans) sp.len=int(spans_size/ptrSize) sp.cap=int(spans_size/ptrSize) }
强烈建议所有程序员都学习一下虚拟存储器的相关知识(推荐《深入理解计算机系统》),很多误解都源自对系统层面的认知匮乏。下面,我们用一个简单示例来澄清有关内存分配的几个常见误解。
test.go
package main
import( “fmt” “os” “github.com/shirou/gopsutil/process” )
var ps*process.Process
// 输出内存状态信息 func mem(n int) { if ps==nil{ p,err:=process.NewProcess(int32(os.Getpid())) if err!=nil{ panic(err) }
ps=p
}
mem, _ :=ps.MemoryInfoEx() fmt.Printf(“%d.VMS: %d MB,RSS: %d MB\n”,n,mem.VMS>>20,mem.RSS>>20) }
func main() { //1. 初始化结束后的内存状态 mem(1)
//2. 创建一个10*1MB数组后的内存状态
data:=new([10][1024*1024]byte) mem(2)
//3. 填充该数组过程中的内存状态
for i:=range data{ for x,n:=0,len(data[i]);x<n;x++ { data[i][x] =1 }
mem(3)
}
}
编译后执行:
1.VMS: 5 MB,RSS:1 MB 2.VMS:15 MB,RSS:1 MB 3.VMS:15 MB,RSS:2 MB 3.VMS:15 MB,RSS:3 MB 3.VMS:15 MB,RSS:4 MB 3.VMS:15 MB,RSS:5 MB 3.VMS:15 MB,RSS:6 MB 3.VMS:15 MB,RSS:7 MB 3.VMS:15 MB,RSS:8 MB 3.VMS:15 MB,RSS:9 MB 3.VMS:15 MB,RSS:10 MB 3.VMS:15 MB,RSS:11 MB
按序号对照输出结果:
1.尽管初始化时预留了544 GB的虚拟地址空间,但并没有分配内存。
2.操作系统大多采取机会主义分配策略,申请内存时,仅承诺但不立即分配物理内存。
3.物理内存分配在写操作导致缺页异常调度时发生,而且是按页提供的。
注意:不同操作系统,可能会存在一些差异。